Repository navigation
diag: say WHY the sparse sleep bridge did nothing (#737) - #789
Merged
Merged
Conversation
A reporter's WHOOP 5.0 night logged: gate run=-1 DROPPED gate=sparseBridge sparse=true gapMin=90 runsBefore=13 runsAfter=13 then had 8 of 13 runs discarded by minSleepMin=60, under-counting asleep time while the in-bed span (their 'bedtime') stayed correct - exactly the mismatch they reported. The bridge exists to re-stitch fragments BEFORE that gate, and it changed nothing, but the trace gave no reason. Three very different causes are possible - gap over the 90-min tolerance, HR outside the sleep band, or the fragments not being adjacent sleep runs at all - and no fix could be chosen responsibly without knowing which. Loosening minSleepMin or bridging across active runs both change sleep scoring for every user. Add sparseBridgeAttempts(): a per-pair explanation that mirrors bridgeSparseSleep's rule EXACTLY (same adjacency walk, same gap >= 0 && gap <= sparseBridgeGapMin*60, same HR-band check), emitting gate sparseBridgePair pair=0 gapMin=12 hrInSleepBand=true reason=bridged per considered pair - and, when NO pair was ever considered, saying so in words, because 'fragments separated by active runs' is itself the diagnosis and the old before/after counts could not express it. Tests pin the diagnostic against the real bridge rather than in isolation: an agreement invariant (bridged-pair count == the bridge's actual run reduction) is asserted on every case, including a three-run chain where a naive implementation that failed to re-walk the growing run would drift. Also covered: gapTooLong, hrOutOfBand, the reporter's active-run shape, and the non-sparse no-op. Pure + CI-covered (StrandAnalytics). No scoring change - diagnostics only.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…r#789) A reporter's WHOOP 5.0 night logged: gate run=-1 DROPPED gate=sparseBridge sparse=true gapMin=90 runsBefore=13 runsAfter=13 then had 8 of 13 runs discarded by minSleepMin=60, under-counting asleep time while the in-bed span (their 'bedtime') stayed correct - exactly the mismatch they reported. The bridge exists to re-stitch fragments BEFORE that gate, and it changed nothing, but the trace gave no reason. Three very different causes are possible - gap over the 90-min tolerance, HR outside the sleep band, or the fragments not being adjacent sleep runs at all - and no fix could be chosen responsibly without knowing which. Loosening minSleepMin or bridging across active runs both change sleep scoring for every user. Add sparseBridgeAttempts(): a per-pair explanation that mirrors bridgeSparseSleep's rule EXACTLY (same adjacency walk, same gap >= 0 && gap <= sparseBridgeGapMin*60, same HR-band check), emitting gate sparseBridgePair pair=0 gapMin=12 hrInSleepBand=true reason=bridged per considered pair - and, when NO pair was ever considered, saying so in words, because 'fragments separated by active runs' is itself the diagnosis and the old before/after counts could not express it. Tests pin the diagnostic against the real bridge rather than in isolation: an agreement invariant (bridged-pair count == the bridge's actual run reduction) is asserted on every case, including a three-run chain where a naive implementation that failed to re-walk the growing run would drift. Also covered: gapTooLong, hrOutOfBand, the reporter's active-run shape, and the non-sparse no-op. Pure + CI-covered (StrandAnalytics). No scoring change - diagnostics only.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A reporter's WHOOP 5.0 night (#737 — "asleep time is incorrect, but bedtime is accurate") logged:
Asleep time is the sum of the kept fragments, so it under-counts, while the in-bed span (their "bedtime") stays right — exactly the mismatch they described.
bridgeSparseSleep(#308) exists to re-stitch fragments beforeminSleepMin, and it changed nothing — but the trace gives no reason.Three very different causes are possible:
No fix can be chosen responsibly without knowing which. Loosening
minSleepMinon the sparse path and bridging across active runs are mutually exclusive, and both change sleep scoring for every user — the class of change this repo's physiological-signal rule exists to gate (the withdrawn PPG→HR #194 is the precedent).What this adds
sparseBridgeAttempts()— a per-pair explanation that mirrors the bridge's rule exactly (same adjacency walk, samegap >= 0 && gap <= sparseBridgeGapMin*60, same HR-band check):And when no pair was ever considered, it says so in words — because "fragments separated by active runs" is itself the diagnosis, and
runsBefore/runsAftercannot express it.Tests
Pinned against the real bridge, not asserted in isolation. Every case asserts the agreement invariant — bridged-pair count must equal
bridgeSparseSleep's actual run reduction — including a three-run chain, where a naive diagnostic that failed to re-walk the growing merged run would silently drift. PlusgapTooLong,hrOutOfBand, the reporter's active-run shape, and the non-sparse no-op.Band arithmetic verified against the real constant (
hrSleepBandMult = 1.05): 50 bpm vs baseline 70 → in band (≤73.5); 110 bpm vs 60 → out of band (>63.0).Scope
Diagnostics only — no scoring change. Pure and CI-covered (
StrandAnalytics), so unlike the app-target work this is genuinely verified.Deliberately not included: any change to
minSleepMinor the bridge's adjacency rule. Those wait for this trace to name the cause — and for the reporter to identify which night looked wrong (their pasted log is truncated mid-line, and the 13 runs total ~22.7 h, so they span the whole analysis window rather than one night).